# This file is part of Neuroinfo Toolkit.
#
# Neuroinfo Toolkit is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Neuroinfo Toolkit is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Neuroinfo Toolkit.  If not, see <http://www.gnu.org/licenses/>.

import struct
import neuro.filesystem as filesystem
import neuro.system as system
from neuro.base import Object
from neuro.models.resource import Resource
from neuro.command import Command
from neuro.models.xml import XML
from neuro.exceptions import IOException
from neuro.exceptions import BaseException
from neuro.exceptions import IllegalArgumentException
from neuro.exceptions import FileFormatException

class Dicom(Resource):
	'''
	DICOM image
	'''
	header_map = {}

	def __init__(self, input, checkFormat=True):
		'''
		Constructor ::
		
			from neuro.models.dicom import Dicom
		
			dicom = Dicom("/path/to/dicom.dcm")
		
		:param input: File name
		:type input: str
		:param checkFormat: Check format on load
		:type checkFormat: bool
		'''
		## --- inheritance
		Resource.__init__(self, input)
		
		## --- initialize instance variables
		self._imageUID = None
		self._studyUID = None
		self._seriesUID = None
		self._header = None
		self._imageNo = None
		self._tr = None
		self._te = None
		self._thickness = None
		self._description = None
		self._sequence = None
		self._seriesNo = None
		self._type = None
		self._flip = None
		self._fov = None
		self._x = None
		self._y = None

		## --- import mixed input variable
		if(isinstance(input, basestring)):
			self._importFile(input, checkFormat=checkFormat)
		else:
			raise IllegalArgumentException("Must provide a filename to DICOM constructor")

	def _importFile(self, filename, checkFormat=True):
		'''
		Import DICOM from file
							
		:param filename: File name
		:type filename: str
		:raises: :class:`~neuro.exceptions.IOException`, 
				 :class:`~neuro.exceptions.ResourceFormatException`
		'''
		filename = filesystem.canonical(filename)

		## --- check if input file is readable
		if(not filesystem.isReadable(filename)):
			raise IOException(IOException.READ, filename)

		## --- check file format
		if(checkFormat and not Dicom.checkFormat(filename)):
			raise FileFormatException(filename)
		
		self._filename = filename
	
	@staticmethod 
	def checkFormat(filename):
		'''
		Check file format validity ::
		
			>>> from neuro.models.dicom import Dicom
			>>> Dicom.checkFormat("/path/to/dicom.dcm")
			True
			
		:param filename: File name
		:type filename: str
		:Returns: Format validity
		:rtype: bool
		:raises: :class:`~neuro.exceptions.IOException`,
				 :class:`~neuro.exceptions.FileNotFoundException`, 
				 :class:`~neuro.exceptions.FileFormatException`
		'''
		Resource.checkFormat(filename)

		filename = filesystem.canonical(filename)

		## --- check if input file is readable
		if(not filesystem.isReadable(filename)):
			raise IOException(IOException.READ, filename)

		h = open(filename, 'rb')
		
		try:	 
			h.seek(128)
			dat = struct.unpack("cccc", h.read(4))
			dicm = "".join(dat)
		except:
			return False
		
		if(dicm == "DICM"):
			return True
		else:
			return False
		
	def getHeaderField(self, id, by_name=True):
		'''
		Retrieve a header field from DICOM file ::
		
			>>> dicom.getHeaderField("StudyInstanceUID")
			'1.3.12.2.1107.5.2.32.35380.30000011102718412821800000004'
			
		:param id: Header identifier
		:type id: str
		:param by_name: Get header field by name instead of tag (not implemented)
		:type by_name: bool
		:returns: Header field value
		:rtype: str
		:raises: :class:`~neuro.command.CommandNotFoundException`,
				 :class:`~neuro.command.CommandFailedException`,
				 :class:`DicomHeadersException`
		'''
		## --- validate inputs
		if(not isinstance(id, basestring)):
			raise IllegalArgumentException("ID must be an instance of str")

		id = id.strip()

		if(id == ""):
			raise IllegalArgumentException("ID cannot be empty")

		if(self._header == None):
			system.check("dcm2xml", "DCMTK")
			command = Command("dcm2xml " + self._filename)
			command.execute()

			if(command.getStdout() == ""):
				raise DicomHeadersException(DicomHeadersException.MISSING_TAG, "all", self)

			self._header = XML(command.getStdout())

		value = self._header.xpath('/file-format/data-set/element[@name="' + id + '"]/text()')

		if(value == None):
			raise DicomHeadersException(DicomHeadersException.MISSING_TAG, self, id)

		value = str(value).strip()

		if(value == ""):
			raise DicomHeadersException(DicomHeadersException.BLANK_TAG, self, id)
		
		return value

	def getStudyUID(self):
		'''
		Retrieve study (session) UID ::
		
			>>> dicom.getStudyUID()
			'1.3.12.2.1107.5.2.32.35380.30000011102718412821800000004'

		:returns: DICOM study UID
		:rtype: str
		:raises: :class:`~neuro.command.CommandNotFoundException`,
				 :class:`~neuro.command.CommandFailedException`,
				 :class:`DicomHeadersException`
		'''
		if(self._studyUID == None):
			self._studyUID = self.getHeaderField("StudyInstanceUID")

		return self._studyUID

	def getSeriesUID(self):
		'''
		Retrieve series UID ::

			>>> dicom.getSeriesUID()
			'1.3.12.2.1107.5.2.32.35380.201110271603332331423700.0.0.0'
		
		:returns: DICOM series UID
		:rtype: str
		:raises: :class:`~neuro.command.CommandNotFoundException`,
				 :class:`~neuro.command.CommandFailedException`,
				 :class:`DicomHeadersException`
		'''
		if(self._seriesUID == None):
			self._seriesUID = self.getHeaderField("SeriesInstanceUID")

		return self._seriesUID

	def getUID(self):
		'''
		Retrieve UID of DICOM image ::
		
			>>> dicom.getUID()
			'1.3.12.2.1107.5.2.32.35380.2011102716042229083225705'
		
		:returns: DICOM image UID
		:rtype: str
		:raises: :class:`~neuro.command.CommandNotFoundException`,
				 :class:`~neuro.command.CommandFailedException`,
				 :class:`DicomHeadersException`
		'''
		if(self._imageUID == None):
			self._imageUID = self.getHeaderField("SOPInstanceUID")

		return self._imageUID

	def getX(self):
		'''
		Retrieve X dimension of DICOM image ::
		
			>>> dicom.getX()
			504
		
		:returns: X dimension
		:rtype: int
		:raises: :class:`~neuro.command.CommandNotFoundException`,
				 :class:`~neuro.command.CommandFailedException`,
				 :class:`DicomHeadersException`
		''' 		
		if(self._x == None):
			self._x = int(self.getHeaderField("Rows"))
		
		return self._x
		
	def getY(self):
		'''
		Retrieve Y dimension of DICOM image ::
		
			>>> dicom.getY()
			504
		
		:returns: Y dimension
		:rtype: int
		:raises: :class:`~neuro.command.CommandNotFoundException`,
				 :class:`~neuro.command.CommandFailedException`,
				 :class:`DicomHeadersException`
		''' 
		if(self._y == None):
			self._y = int(self.getHeaderField("Columns"))
		
		return self._y
	
	def getFlipAngle(self):
		'''
		Retrieve flip angle of DICOM image ::
		
			>>> dicom.getFlipAngle()
			85.0
		
		:returns: Flip angle
		:rtype: float
		:raises: :class:`~neuro.command.CommandNotFoundException`,
				 :class:`~neuro.command.CommandFailedException`,
				 :class:`DicomHeadersException`
		''' 
		if(self._flip == None):
			self._flip = float(self.getHeaderField("FlipAngle"))
			
		return self._flip
		
	def getFieldOfView(self):
		'''
		Retrieve field of view of DICOM image ::
		
			>>> dicom.getFieldOfView()
			100.0
		
		:returns: Field of view
		:rtype: float
		:raises: :class:`~neuro.command.CommandNotFoundException`,
				 :class:`~neuro.command.CommandFailedException`,
				 :class:`DicomHeadersException`
		'''
		if(self._fov == None):
			self._fov = float(self.getHeaderField("PercentPhaseFieldOfView"))
		
		return self._fov

	def getSliceThickness(self):
		'''
		Retrieve slice thickness of DICOM image ::
		
			>>> dicom.getSliceThickness()
			3.0
		
		:returns: Slice thickness (in mm)
		:rtype: float
		:raises: :class:`~neuro.command.CommandNotFoundException`,
				 :class:`~neuro.command.CommandFailedException`,
				 :class:`DicomHeadersException`
		'''
		if(self._thickness == None):
			self._thickness = float(self.getHeaderField("SliceThickness"))
		
		return self._thickness
	
	def getEchoTime(self):
		'''
		Retrieve echo time (TE) of DICOM image ::
		
			>>> dicom.getEchoTime()
			30.0
		
		:returns: Echo time	
		:rtype: float
		:raises: :class:`~neuro.command.CommandNotFoundException`,
				 :class:`~neuro.command.CommandFailedException`,
				 :class:`DicomHeadersException`
		''' 
		## --- check for missing URI
		if(self._te == None):
			self._te = float(self.getHeaderField("EchoTime"))
		
		return self._te
	
	def getRepetitionTime(self):
		'''
		Retrieve repetition time (TR) of DICOM image.
		
			>>> dicom.getRepetitionTime()
			3000.0
		
		:returns: Repetition time
		:rtype: float
		:raises: :class:`~neuro.command.CommandNotFoundException`,
				 :class:`~neuro.command.CommandFailedException`,
				 :class:`DicomHeadersException`
		''' 
		if(self._tr == None):
			self._tr = float(self.getHeaderField("RepetitionTime"))
		
		return self._tr
	
	def getSequence(self):
		'''
		Retrieve sequence name from DICOM image ::
		
			>>> dicom.getSequence()
			'epfid2d1_72'
		
		:returns: MR series sequence
		:rtype: int
		:raises: :class:`~neuro.command.CommandNotFoundException`,
				 :class:`~neuro.command.CommandFailedException`,
				 :class:`DicomHeadersException`
		'''
		if(self._sequence == None):
			self._sequence = self.getHeaderField("SequenceName")
		
		return self._sequence
	
	def getSeriesDescription(self):
		'''
		Retrieve series description from DICOM image ::
		
			>>> dicom.getSeriesDescription()
			'fMRI_resting_state'
		
		:returns: MR series sequence description
		:rtype: str
		:raises: :class:`~neuro.command.CommandNotFoundException`,
				 :class:`~neuro.command.CommandFailedException`,
				 :class:`DicomHeadersException`
		''' 		
		if(self._description == None):
			self._description = self.getHeaderField("SeriesDescription")
		
		return self._description
	
	def getSeriesNumber(self):
		'''
		Retrieve series number from DICOM image ::
		
			>>> dicom.getSeriesNumber()
			21
		
		:returns: DICOM series number	
		:rtype: int
		:raises: :class:`~neuro.command.CommandNotFoundException`,
				 :class:`~neuro.command.CommandFailedException`,
				 :class:`DicomHeadersException`
		''' 
		if(self._seriesNo == None):
			self._seriesNo = int(self.getHeaderField("SeriesNumber"))
		
		return self._seriesNo
	
	def getImageNumber(self):
		'''
		Get image number ::
		
			>>> dicom.getImageNumnber()
			14
		
		:returns: DICOM image number within series
		:rtype: int
		:raises: :class:`~neuro.command.CommandNotFoundException`,
				 :class:`~neuro.command.CommandFailedException`,
				 :class:`DicomHeadersException`
		'''
		if(self._imageNo == None):
			self._imageNo = int(self.getHeaderField("InstanceNumber"))
		
		return self._imageNo

class DicomSession(Object):
	'''
	DICOM session
	
	While it is possible to create and populate a DicomSession manually ::
		
		from neuro.models.dicom import DicomSession
		
		session = DicomSession()
		session.addImage(Dicom("/path/to/dicom01.dcm"))
		session.addImage(Dicom("/path/to/dicom02.dcm"))
		...
	
	you may find it more convenient to let a 
	:class:`~neuro.models.scanners.DicomScanner` scan a directory for DICOM 
	images and compile a dict of :class:`DicomSession` objects that you can 
	index by StudyInstanceUID ::
	
		>>> from neuro.models.scanners import DicomScanner
		>>> listing = DicomScanner.scan("/path/to/dicom/images")
		>>> for item in listing.items():
		...	print item
		('1.3.12.2.1107.5.2.32.35380.30000011102718412821800000004', <neuro.models.dicom.DicomSession object at 0x2009950>)
	'''
	
	def __init__(self):
		self._uid = None
		self._series = {}
		
	def addImage(self, image):
		'''
		Add DICOM image to MR Session
		
		:param image: DICOM image object
		:type image: :class:`Dicom`
		:raises: :class:`DicomSessionException`,
				 :class:`DicomHeadersException`
		'''
		if(isinstance(image, basestring)):
			image = Dicom(image)
		elif(not isinstance(image, Dicom)):
			raise IllegalArgumentException("Image must be an instance of str or Dicom")

		seriesNo = image.getSeriesNumber()
		uid = image.getStudyUID()
		
		if(self._uid == None):
			self._uid = uid
		elif(self._uid != uid):
			raise DicomSessionException(DicomSessionException.MISMATCH, self)

		if(seriesNo not in self._series):
			self._series[seriesNo] = DicomSeries()

		self._series[seriesNo].addImage(image)

	def __iter__(self):
		return self._series.iteritems()

	def iteritems(self):
		return self._series.iteritems()
	
	def getSeriesByNumber(self, number):
		'''
		Get series by number ::
		
			>>> session.getSeriesByNumber(12)
			<neuro.models.dicom.DicomSeries object at 0x1b7ca10>
		
		:param number:
		:type number: int
		:rtype: :class:`DicomSeries`
		:raises: :class:`SeriesNotFoundException`
		'''
		if(not isinstance(number, int)):
			raise IllegalArgumentException("Series number must be an instance of int")
		elif(number <= 0):
			raise IllegalArgumentException("Series number cannot be less than 1")
		
		if(number not in self._series):
			raise SeriesNotFoundException(SeriesNotFoundException.NUM, str(number), self)
		
		return self._series[number]
	
	def getSeriesByDescription(self, description):
		'''
		Retrieve a list of DICOM series objects by description ::

			>>> session.getSeriesByDescription("fMRI_resting_state")
			<neuro.models.dicom.DicomSeries object at 0x1b7ca10>
			
		:param description:
		:type description: str
		:returns: (Series Number, Series)
		:rtype: list
		:raises: :class:`SeriesNotFoundException`
		'''
		if(not isinstance(description, basestring)):
			raise IllegalArgumentException("Description must be of type str")
		
		if(description == ""):
			raise IllegalArgumentException("Description cannot be null")
		
		result = {}
		
		for num,series in self._series.iteritems():
			if(series.getSeriesDescription().lower() == description.lower()):
				result[num] = series

		if(len(result) == 0):
			raise SeriesNotFoundException(SeriesNotFoundException.DESC, description, self)
		
		return result

	def getNumSeries(self):
		'''
		Get number of series/scans in DicomSession :: 
		
			>>> session.getNumSeries()
			1

		:returns: Number of series/scans in study/session
		:rtype: int
		'''
		return len(self._series)

	def getSeriesDescriptions(self):
		'''
		Get all :class:`DicomSeries` descriptions as list :: 
		
			>>> session.getSeriesDescriptions()
			['fMRI_resting_state']
			
		:returns: List of DICOM series descriptions
		:rtype: list
		'''
		d = list()

		for num,series in self._series.iteritems():
			d.append(series.getSeriesDescription())

		return d

	def getUID(self):
		'''
		Get DICOM session StudyInstanceUID ::
		
			>>> session.getUID()
			'1.3.12.2.1107.5.2.32.35380.30000011102718412821800000004'

		:rtype: str
		'''
		return self._uid

	def copyTo(self, dir):
		'''
		Copy entire DICOM session to a different location
		
		:param dir: Where to copy DICOM session to
		:type dir: str
		'''
		if(len(self._series) == 0):
			raise DicomSeriesException(DicomSessionException.EMPTY, self)
		elif(not isinstance(dir, basestring)):
			raise IllegalArgumentException("Directory must be an instance of str")

		for (uid, series) in self:
			series.copyTo(dir)

	def moveTo(self, dir):
		'''
		Move entire DICOM session to a different location

		:param dir: Where to move DICOM session to
		:type dir: str
		'''
		if(len(self._series) == 0):
			raise DicomSeriesException(DicomSessionException.EMPTY, self)
		elif(not isinstance(dir, basestring)):
			raise IllegalArgumentException("Directory must be an instance of str")

		for (uid, series) in self:
			series.moveTo(dir)

class DicomSeries(Object):
	'''
	DICOM series
	
	.. note:: Some community members refer to a DICOM *series* as a *scan*. For example a BOLD scan, an anatomical scan, or a MPRAGE.
	
	While it is possible to create and populate a DicomSeries manually ::
		
		from neuro.models.dicom import DicomSeries
		
		series = DicomSeries()
		series.addImage(Dicom("/path/to/dicom01.dcm"))
		series.addImage(Dicom("/path/to/dicom02.dcm"))
		...
	
	you may find it more convenient to let a 
	:class:`~neuro.models.scanners.DicomScanner` scan a directory for DICOM 
	images and compile a dict of :class:`DicomSession` objects that you can 
	index by StudyInstanceUID ::
	
		>>> from neuro.models.scanners import DicomScanner
		>>> listing = DicomScanner.scan("/path/to/dicom/images")
		>>> for item in listing.items():
		...	print item
		('1.3.12.2.1107.5.2.32.35380.30000011102718412821800000004', <neuro.models.dicom.DicomSession object at 0x2009950>)
	
	You may then access an individual :class:`DicomSeries` object e.g., ::
		
		>>> session.getSeriesByDescription("fMRI_resting_state")
		<neuro.models.dicom.DicomSeries object at 0x1b7ca10>
	'''
	
	def __init__(self):
		self._images = {}
		self._repr = None
		self._seriesNo = None
		self._uid = None
		self._numImages = None
		self._maxIntensity = None
		self._minIntensity = None

	def addImage(self, image):
		'''
		Add image to series

		:param image: DICOM image object
		:type image: :class:`Dicom`
		:raises: DicomHeadersException
		'''
		if(isinstance(image, basestring)):
			image = Dicom(image)
		elif(not isinstance(image, Dicom)):
			raise IllegalArgumentException("Input image must be instance of Dicom")

		imageNo = image.getImageNumber()
		uid = image.getSeriesUID()
		
		if(self._uid == None):
			self._uid = uid
		elif(self._uid != uid):
			raise DicomSeriesException(DicomSeriesException.MISMATCH, self)

		self._images[imageNo] = image
		
		## --- image to represent the series
		if(self._repr == None):
			self._repr = image

	def __iter__(self):
		'''
		Iterator for Dicom image objects

		:returns: (image number, :class:`Dicom`)
		:rtype: list
		'''
		return self._images.iteritems()
	
	def getSeriesDescription(self):
		'''
		Get series description ::
		
			>>> series.getSeriesDescription()
			'fMRI_resting_state'
		
		:rtype: str
		:raises: :class:`DicomSeriesException`,
				 :class:`DicomHeadersException`
		'''
		if(len(self._images) == 0):
			raise DicomSeriesException(DicomSeriesException.EMPTY, self)

		return self._repr.getSeriesDescription()
	
	def getSequence(self):
		'''
		Get sequence name ::
		
			>>> series.getSequence()
			'epfid2d1_72'
		
		:rtype: str
		:raises: :class:`DicomSeriesException`,
				 :class:`DicomHeadersException`
		'''
		if(len(self._images) == 0):
			raise DicomSeriesException(DicomSeriesException.EMPTY, self)
		
		return self._repr.getSequence()
	
	def getEchoTime(self):
		'''
		Get echo time ::
			
			>>> series.getEchoTime()
			30.0
			
		:rtype: float
		:raises: :class:`DicomSeriesException`,
				 :class:`DicomHeadersException`
		'''
		if(len(self._images) == 0):
			raise DicomSeriesException(DicomSeriesException.EMPTY, self)
		
		return self._repr.getEchoTime()
	
	def getRepetitionTime(self):
		'''
		Get repetition time ::
		
			>>> series.getRepetitionTime()
			3000.0
			
		:rtype: float
		:raises: :class:`DicomSeriesException`,
				 :class:`DicomHeadersException`
		'''
		if(len(self._images) == 0):
			raise DicomSeriesException(DicomSeriesException.EMPTY, self)
		
		return self._repr.getRepetitionTime()

	def getSeriesUID(self):
		'''
		Get DICOM series UID ::
		
			>>> series.getSeriesUID()
			'1.3.12.2.1107.5.2.32.35380.201110271603332331423700.0.0.0'

		:rtype: str
		:raises: :class:`DicomSeriesException`,
				 :class:`DicomHeadersException`
		'''
		if(len(self._images) == 0):
			raise DicomSeriesException(DicomSeriesException.EMPTY, self)

		return self._repr.getSeriesUID()
		
	def getSeriesNumber(self):
		'''
		Retrieve the series number ::
		
			>>> series.getSeriesNumber()
			21
			
		:rtype: int
		:raises: :class:`DicomSeriesException`,
				 :class:`DicomHeadersException`
		'''
		if(len(self._images) == 0):
			raise DicomSeriesException(DicomSeriesException.EMPTY, self)
		
		return self._repr.getSeriesNumber()
		
	def getNumImages(self):
		'''
		Retrieve the number of images in series ::
		
			>>> series.getNumImages()
			124
			
		:rtype: int
		'''
		return len(self._images)
	
	def iteritems(self):
		'''
		Get items as dictionary for iteration
			
		:returns: (image_num, :class:`Dicom`)
		:rtype: dict
		'''
		return self._images.iteritems()
	
	def getImageByNumber(self, number):
		'''
		Get DICOM image by number :: 
		
			>>> series.getImageByNumber(1)
			<neuro.models.dicom.Dicom object at 0x1745450>
		
		:returns: DICOM image object
		:rtype: :class:`Dicom`
		:raises: :class:`DicomSeriesException`, 
				 :class:`ImageNotFoundException`
		'''
		if(len(self._images) == 0):
			raise DicomSeriesException(DicomSeriesException.EMPTY, self)
		elif(not isinstance(number, int)):
			raise IllegalArgumentException("Image number must be an instance of int")
		elif(number < 1):
			raise IllegalArgumentException("Image number must be greater than 1")
		
		if(number not in self._images):
			raise ImageNotFoundException(str(number), self)
		
		return self._images[number]

	def copyTo(self, dir):
		'''
		Copy entire DICOM series to different location

		:param dir: Where to copy DICOM series to
		:type dir: str
		'''
		if(len(self._images) == 0):
			raise DicomSeriesException(DicomSeriesException.EMPTY, self)
		elif(not isinstance(dir, basestring)):
			raise IllegalArgumentException("Directory must be an instance of str")
		
		for (num, image) in self:
			image.copyTo(dir)

	def moveTo(self, dir):
		'''
		Move entire DICOM series to different location

		:param dir: Where to move DICOM series to
		:type dir: str
		'''
		if(len(self._images) == 0):
			raise DicomSeriesException(DicomSeriesException.EMPTY, self)
		elif(not isinstance(dir, basestring)):
			raise IllegalArgumentException("Directory must be an instance of str")

		for (num, image) in self:
			image.moveTo(dir)

class DicomHeadersException(BaseException):
	'''
	DICOM headers exception
	'''
	MISSING_TAG=1
	BLANK_TAG=2

	def __init__(self, type, dicom, tag):
		'''
		Constructor

		:param type: DicomHeadersException.MISSING_TAG, etc.
		:type type: int
		:param dicom: Dicom object
		:type dicom: :class:`Dicom`
		:param tag: Header tag
		:type tag: str
		'''
		BaseException.__init__(self)

		if(not isinstance(type, int)):
			raise IllegalArgumentException("Exception type must be an instance of int")
		elif(not isinstance(dicom, Dicom)):
			raise IllegalArgumentException("Input dicom parameter must be an instance of Dicom")
		elif(not isinstance(tag, basestring)):
			raise IllegalArgumentException("DICOM header tag must be an instance of str")

		tag = tag.strip()

		if(type < 1 or type > 2):
			raise IllegalArgumentException("Exception type must be DicomHeadersException.MISSING_TAG or DicomHeadersException.BLANK_TAG")
		elif(tag == ""):
			raise IllegalArgumentException("DICOM header tag cannot be empty")

		self._type = type
		self._dicom = dicom
		self._tag = tag

	def getType(self):
		'''
		Get exception type

		:rtype: int
		'''
		return self._type

	def getDicom(self):
		'''
		Get the DICOM image that triggered this exception

		:rtype: :class:`Dicom`
		'''
		return self._dicom

	def getTag(self):
		'''
		Get the header tag that triggered this exception

		:rtype: str
		'''
		return self._tag

	def getMessage(self):
		'''
		Get custom message

		:rtype: str
		'''
		if(self._type == DicomHeadersException.MISSING_TAG):
			return "Missing header tag \"" + self._tag + "\" for DICOM file: " + self._dicom.getFilename()
		if(self._type == DicomHeadersException.BLANK_TAG):
			return "Header tag \"" + self._tag + "\" for DICOM file: " + self._dicom.getFilename() + " is empty"

class DicomSessionException(BaseException):
	'''
	DICOM session exception
	'''
	EMPTY=1
	MISMATCH=2

	def __init__(self, type, session):
		'''
		Constructor

		:param type: DicomSessionException.EMPTY, DicomSessionException.MISMATCH, etc.
		:type type: int
		'''
		BaseException.__init__(self)

		if(not isinstance(type, int)):
			raise IllegalArgumentException("Exception type must be an instance of int")
		elif(not isinstance(session, DicomSession)):
			raise IllegalArgumentException("Session parameter must be an instance of DicomSession")

		if(type < 1 or type > 2):
			raise IllegalArgumentException("Exception type must be DicomSessionException.EMPTY or DicomSessionException.MISMATCH")

		self._type = type
		self._session = session

	def getType(self):
		'''
		Get exception type

		:rtype: int
		'''
		return self._type

	def getSession(self):
		'''
		Get DicomSession that triggered this exception

		:rtype: :class:`DicomSession`
		'''
		return self._session

	def getMessage(self):
		'''
		Get custom message

		:rtype: str
		'''
		if(self._type == DicomSessionException.EMPTY):
			return "DICOM session is empty i.e. no DICOM series"
		elif(self._type == DicomSessionException.MISMATCH):
			return "Tried to populate DICOM session with images that contain multiple study UIDs"

class DicomSeriesException(BaseException):
	'''
	DICOM series exception
	'''
	EMPTY=1
	MISMATCH=2

	def __init__(self, type, series):
		'''
		Constructor

		:param type: DicomSeriesException.EMPTY, DicomSeriesException.MISMATCH, etc.
		:type type: int
		'''
		BaseException.__init__(self)

		if(not isinstance(type, int)):
			raise IllegalArgumentException("Exception type must be an instance of int")
		elif(not isinstance(session, DicomSession)):
			raise IllegalArgumentException("Series parameter must be an instance of DicomSeries")

		if(type < 1 or type > 2):
			raise IllegalArgumentException("Exception type must be DicomSeriesException.EMPTY or DicomSeriesException.MISMATCH")

		self._type = type
		self._series = series

	def getType(self):
		'''
		Get exception type

		:rtype: int
		'''
		return self._type

	def getSeries(self):
		'''
		Get DicomSeries that triggered this exception

		:returns: :class:`DicomSeries`
		'''
		return self._series

	def getMessage(self):
		'''
		Get custom message

		:rtype: str
		'''
		if(self._type == DicomSeriesException.EMPTY):
			return "DICOM series is empty i.e. no DICOM images"
		elif(self._type == DicomSeriesException.MISMATCH):
			return "Tried to populate DICOM series with images that contain multiple series UIDs"

class SeriesNotFoundException(BaseException):
	'''
	DICOM series not found
	'''
	NUM=1
	DESC=2
	
	def __init__(self, type, index, session):
		'''
		Constructor

		:param type: SeriesNotFoundException.NUM, SeriesNotFoudException.DESC, etc.
		:type type: int
		:param index: Index
		:type index: str
		:param session: :class:`DicomSession` object
		:type session: :class:`DicomSession`
		'''
		BaseException.__init__(self)
		
		if(not isinstance(type, int)):
			raise IllegalArgumentException("Exception type must be an instance of int")
		elif(not isinstance(index, basestring)):
			raise IllegalArgumentException("DICOM series index must be an instance of str")
		elif(not isinstance(session, DicomSession)):
			raise IllegalArgumentException("Session parameter must be an instance of DicomSession")

		if(type < 1 or type > 2):
			raise IllegalArgumentException("Exception type must be SeriesNotFoundException.NUM or SeriesNotFoundException.DESC")
		
		self._type = type
		self._index = index
		self._session = session

	def getType(self):
		'''
		Get the exception type

		:rtype: int
		'''
		return self._type

	def getIndex(self):
		'''
		Get the series index that triggered this exception

		:rtype: str
		'''
		return self._index

	def getSession(self):
		'''
		Get the DicomSession that triggered this exception

		:rtype: :class:`DicomSession`
		'''
		return self._session

	def getMessage(self):
		'''
		Get custom message

		:rtype: str
		'''
		if(self._type == SeriesNotFoundException.NUM):
			return "No such series by number \"" + self._index + "\" for DICOM session \"" + self._session.getUID() + "\""
		elif(self._type == SeriesNotFoundException.DESC):
			return "No such series by description \"" + self._index + "\" for DICOM session \"" + self._session.getUID() + "\""

class ImageNotFoundException(BaseException):
	'''
	DICOM image not found
	'''

	def __init__(self, index, series):
		'''
		Constructor

		:param index: Index
		:type index: int
		:param series: :class:`DicomSeries` object
		:type series: :class:`DicomSeries`
		'''
		BaseException.__init__(self)

		if(not isinstance(index, int)):
			raise IllegalArgumentException("DICOM image index must be an instance of int")
		elif(not isinstance(session, DicomSeries)):
			raise IllegalArgumentException("Series parameter must be an instance of DicomSeries")

		self._index = index
		self._series = series

	def getIndex(self):
		'''
		Get the image index that triggered this exception

		:rtype: int
		'''
		return self._index

	def getSeries(self):
		'''
		Get the DicomSeries that triggered this exception

		:rtype: :class:`DicomSeries`
		'''
		return self._series

	def getMessage(self):
		'''
		Get custom message

		:rtype: str
		'''
		return "No such image by number \"" + str(self._index) + "\" for DICOM series \"" + self._series.getUID() + "\""
